package ch.jamme.impl;

import static ch.jamme.util.IntrospectionUtil.*;

import java.lang.reflect.*;
import java.util.*;

import org.jdom.*;

import ch.jamme.conf.*;
import ch.jamme.exception.*;
import ch.jamme.reference.*;
import ch.jamme.util.*;

@SuppressWarnings("unchecked")
class XmlToObjectConvertor {

	protected MarshallerEngine marshallerEngine;
	
	protected MarshallerConfiguration configuration;
	protected ObjectCreator objectCreator;
	protected ObjectToStringConvertor objectToStringConvertor;
	protected FieldSuppressionStrategy fieldSuppressionStrategy;
	protected FieldBindingStrategy fieldBindingStrategy;
	protected FieldNamingStrategy fieldNamingStrategy;
	protected FieldPrimitiveStrategy fieldPrimitiveStrategy;
	protected ClassNormalizer classNormalizer;
	protected NamingConvention namingConvention;
	
	private boolean isDebugEnabled = false;
	
	XmlToObjectConvertor(MarshallerEngine marshallerEngine) {
		this.marshallerEngine = marshallerEngine;
		configuration = marshallerEngine.getConfiguration();
		objectCreator = configuration.getObjectCreator();
		objectToStringConvertor = configuration.getObjectToStringConvertor();
		fieldSuppressionStrategy = configuration.getFieldSuppressionStrategy();
		fieldBindingStrategy = configuration.getFieldBindingStrategy();
		fieldNamingStrategy = configuration.getFieldNamingStrategy();
		fieldPrimitiveStrategy = configuration.getFieldPrimitiveStrategy();
		classNormalizer = configuration.getClassNormalizer();
		namingConvention = configuration.getNamingConvention(); 
		isDebugEnabled = Logger.isDebugEnabledFor(this);
	}
	
	protected <T> T convertXmlToObject(Element element, Class<T> fieldClass) throws UnmarshallException {
		return (T)toObject(element, null, fieldClass);
	}
	
	protected final Object toObject(Element element, Field field) throws UnmarshallException {
		return toObject(element, field, field.getGenericType());
	}
	
	protected final Object toObject(Element element, Field field, Type fieldType) throws UnmarshallException {
		Class<?> fieldClass = asClass(fieldType);
		if (fieldClass != null && fieldPrimitiveStrategy.isPrimitive(fieldClass)) 
			return objectToStringConvertor.toObject(fieldClass, element.getTextTrim());
		
		String idRefAttributeValue = element.getAttributeValue(namingConvention.getIdentifierReferenceAttribute());
		if (idRefAttributeValue != null) {
			if (Logger.isDebugEnabledFor(this)) Logger.debug(this, "Trying to resolve referenced object " + idRefAttributeValue);
			return marshallerEngine.getResolvedObject(idRefAttributeValue, field);
		}
		
		Object object = createObject(element, fieldType, field);
		
		String idAttributeValue = element.getAttributeValue(namingConvention.getIdentifierAttribute());
		if (idAttributeValue != null) {
			if (Logger.isDebugEnabledFor(this)) Logger.debug(this, "Referencing object " + object + " with id " + idAttributeValue);
			marshallerEngine.registerResolveObject(idAttributeValue, object);
		}

		// Populate nested data structures (if any)
		if (object instanceof Collection) return getCollectionValue((Collection<Object>)object, element, field, null);
		if (object instanceof Map) return getMapValue((Map<Object, Object>)object, element, field, fieldType);

		populateObject(element, object);
		return object;
	}
	
	protected void populateObject(Element element, Object object) throws UnmarshallException {
		Class<?> normalizedClass = classNormalizer.getNormalizedClass(object);
		for (Field objectField : getAllFields(normalizedClass)) {
			if (skipField(objectField)) continue;
			if (isDebugEnabled) Logger.debug(this, "Populating field " + objectField + " of " + object);
			Object fieldValue = null;
			try {
				fieldValue = getValueForField(normalizedClass, element, objectField, object);
			} catch (Exception e) {
				manageException("Error accessing field " + objectField + " of " + object, e);
			}
			
			try {
				if (fieldValue != null) {
					if (isDebugEnabled) Logger.debug(this, "Setting value " + fieldValue + " for field " + objectField + " on object " + object);
					setFieldValue(normalizedClass, objectField, object, fieldValue);
				} else if (isDebugEnabled) Logger.debug(this, "Field " + objectField + " on object " + object + " is null");
			} catch (Exception e) {
				manageException("Error setting field " + objectField + " of " + object, e);
			}
		}
		parseUnreferencedChildren(element);
	}
	
	private boolean skipField(Field objectField) {
		if (fieldSuppressionStrategy.suppressField(objectField)) {
			if (isDebugEnabled) Logger.debug(this, "Skipping suppressed field " + objectField);
			return true;
		}
		if (!makeAccesible(objectField)) {
			if (isDebugEnabled) Logger.debug(this, "Skipping not accessible field " + objectField);
			return true;
		}
		return false;
	}
	
	private void parseUnreferencedChildren(Element element) {
		Iterator<?> children = element.getChildren().iterator();
		while (children.hasNext()) {
			Object child = children.next();
			if (!(child instanceof Element)) continue;
			Element childElement = (Element)child;
			String idAttributeValue = childElement.getAttributeValue(namingConvention.getIdentifierAttribute());
			if (idAttributeValue == null) {
				parseUnreferencedChildren(childElement);
				continue;
			}
			if (isDebugEnabled) Logger.debug(this, "Parsing unreferenced child element " + childElement + " with id " + idAttributeValue);
			toObject(childElement, null, null);
		}
	}
	
	private Object getValueForField(Class<?> normalizedClass, Element element, Field field, Object object) throws UnmarshallException {
		String fieldName = field.getName();
		Class<?> fieldClass = field.getType();
		String translatedName = fieldNamingStrategy.translate(normalizedClass, fieldName, false);
		
		if (fieldBindingStrategy.bindAsAttribute(fieldClass, translatedName)) {
			String fieldValueAsAttribute = element.getAttributeValue(translatedName);
			if (fieldValueAsAttribute == null && !fieldName.equals(translatedName)) element.getAttributeValue(fieldName);
			return fieldValueAsAttribute != null ? objectToStringConvertor.toObject(fieldClass, fieldValueAsAttribute) : null;
		}
		
		Element fieldValueAsElement = element.getChild(translatedName);
		if (fieldValueAsElement == null && !fieldName.equals(translatedName)) {
			if (isDebugEnabled) Logger.debug(this, "Fail to find an element with the translated name " + translatedName + "; let's try with the original one " + fieldName);
			fieldValueAsElement = element.getChild(fieldName);
		}
		if (fieldValueAsElement == null) {
			if (isDebugEnabled) Logger.debug(this, "No element with name " + translatedName + " for field " + field + " exists");
			return null;
		}
		element.removeContent(fieldValueAsElement);
		
		if (fieldClass.isArray()) return parseArray(fieldClass, fieldValueAsElement, field);
		if (Iterable.class.isAssignableFrom(fieldClass)) return parseIterable(normalizedClass, fieldValueAsElement, field, object);
		if (Map.class.isAssignableFrom(fieldClass)) return parseMap(normalizedClass, fieldValueAsElement, field, object);
		
		Object unmarshalled = toObject(fieldValueAsElement, field);
		if (unmarshalled instanceof ForwardReference) {
			((ForwardReference)unmarshalled).setAsSingleField(normalizedClass, object);
			return null;
		}
		return unmarshalled;
	}
	
	private Object parseArray(Class<?> fieldClass, Element fieldValueAsElement, Field field) {
		Class<?> itemClass = fieldClass.getComponentType();
		Collection<Object> coll = getCollectionValue(new LinkedList<Object>(), fieldValueAsElement, field, itemClass);
		Object array = Array.newInstance(itemClass, coll.size());
		int count = 0;
		for (Object obj : coll) Array.set(array, count++, obj);
		return array;
	}
	
	private Object parseIterable(Class<?> normalizedClass, Element fieldValueAsElement, Field field, Object object) {
		try {
			// if the object has an already instanciated empty collection use it
			Collection<Object> coll = (Collection)getFieldValue(normalizedClass, field, object);
			if (coll != null) coll.clear();
			Type[] types = getGenericTypes(field);
			return getCollectionValue(coll, fieldValueAsElement, field, types != null && types.length > 0 ? types[0] : null);
		} catch (Exception e) {
			manageException("Error reading collection from field " + field, e);
			return null;
		}
	}
	
	private Object parseMap(Class<?> normalizedClass, Element fieldValueAsElement, Field field, Object object) {
		try {
			// if the object has an already instanciated empty collection use it
			Map<Object, Object> map = (Map)getFieldValue(normalizedClass, field, object);
			if (map != null) map.clear();
			return getMapValue(map, fieldValueAsElement, field, null);
		} catch (Exception e) {
			manageException("Error reading collection from field " + field, e);
			return null;
		}
	}
	
	private Collection<Object> getCollectionValue(Collection<Object> coll, Element fieldValueAsElement, Field field, Type itemType) {
		if (coll == null) coll = (Collection)createObject(fieldValueAsElement, field.getGenericType(), field);
		Iterator<?> i = fieldValueAsElement.getChildren().iterator();
		while (i.hasNext()) {
			Object unmarshalled = toObject((Element)i.next(), null, itemType);
			if (unmarshalled instanceof ForwardReference) ((ForwardReference)unmarshalled).setAsCollectionField(coll);
			else coll.add(unmarshalled);
		}
		return coll;
	}
	
	private Map<? super Object, ? super Object> getMapValue(Map<? super Object, ? super Object> map, Element fieldValueAsElement, Field field, Type fieldType) {
		Type[] types = fieldType != null ? getGenericTypes(fieldType) : getGenericTypes(field);
		Type keyType = types != null && types.length > 0 ? types[0] : null;
		Type valueType = types != null && types.length > 1 ? types[1] : null;
		
		if (map == null) map = (Map)createObject(fieldValueAsElement, field.getGenericType(), field);
		Iterator<?> i = fieldValueAsElement.getChildren().iterator();
		while (i.hasNext()) {
			Element entryElement = (Element)i.next();
			List<?> mapEntry = entryElement.getChildren();
			Object key = toObject((Element)mapEntry.get(0), null, keyType);
			Object value = toObject((Element)mapEntry.get(1), null, valueType);
			if (key instanceof ForwardReference) {
				if (value instanceof ForwardReference) ((ForwardReference)value).setAsMapBothFields(map, ((ForwardReference)key).getIdRef());
				else ((ForwardReference)key).setAsMapKeyField(map, value);
			} else {
				if (value instanceof ForwardReference) ((ForwardReference)value).setAsMapValueField(map, key);
				else map.put(key, value);
			}
		}
		return map;
	}
	
	private Object createObject(Element element, Type fieldType, Field field) {
		String className = element.getAttributeValue(namingConvention.getClassNameAttribute());
		Class<?> objectClass = className != null ? getClassByName(className) : asClass(fieldType);
		return objectCreator.createObject(element, objectClass, field);
	}
	
	private void manageException(String errorMessage, Exception e) throws UnmarshallException {
		errorMessage += " caused by: " + e.getLocalizedMessage();
		if (configuration.isFailSafe()) Logger.error(this, errorMessage, e);
		else throw e instanceof UnmarshallException ? (UnmarshallException)e : new UnmarshallException(errorMessage, e);
	}
}
